Repository navigation
feat(deepnote): one notebook per .deepnote file - #429
Conversation
…d) + add project-id resolver
Chunk 1 of single-notebook migration (§4 partial, §5). No behaviour change.
- Manager caches originals in a nested Map<projectId, Map<notebookId, project>>
so sibling files sharing a project.id no longer clobber each other.
- New API: getOriginalProject(projectId, notebookId) exact/no-fallback,
getAnyProjectEntry(projectId), storeOriginalProject/updateOriginalProject
(3-arg), updateProjectIntegrations iterates all entries.
- Update IDeepnoteNotebookManager and IPlatformDeepnoteNotebookManager; repoint
all project-level read-only callers to getAnyProjectEntry.
- Add canonical readDeepnoteProjectFile and resolveProjectIdFor{File,Notebook}.
- Selection state and init-run tracking intentionally kept (removed in later chunks).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…rop selection machinery Chunk 2 of single-notebook migration (§1 + Cleanup). - deserializeNotebook renders the first non-init notebook (findDefaultNotebook), falling back to the only/init notebook; never composes init. - serializeNotebook resolves the target from document metadata alone (projectId + notebookId required) and looks it up with the exact getOriginalProject, throwing clear errors instead of falling back to a wrong sibling. - detectContentChanges collapses to a single-notebook comparison. - Remove the ?notebook=<id> selection machinery: findCurrentNotebookId, the manager's selection state + interface methods, the explorer's query-param opens and selectNotebookForProject calls, and the tree item's custom resourceUri. - Explorer no longer depends on IDeepnoteNotebookManager. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…k siblings Chunk 3 of single-notebook migration (§0, §2, §3). - Add allocateSiblingUri: the single filesystem-aware, collision-safe sibling filename allocator (bumps -2/-3 before .deepnote, honors an in-batch reserved set, bounded retries). - Add a notebook file factory (buildSingleNotebookFile / buildSiblingNotebookFileUri) for creating sibling single-notebook files (wired into the explorer in a later chunk). - Add DeepnoteMultiNotebookSplitter: on opening a multi-notebook .deepnote file, offer to split it into one new single-notebook file per notebook. The action flushes the editor if dirty, writes all children, migrates the environment selection, then closes the tab and deletes the original to trash. A child-write failure leaves the original intact (write-before-delete). - Wire the splitter into activation with an optional (desktop-only) environment mapper; add a refresh() passthrough on the explorer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
Chunk 4 of single-notebook migration (§6). - Add DeepnoteProjectMetadataPropagator (desktop): given a project id and a project-level mutator, enumerate every sibling .deepnote file on disk (open or closed), apply the change, and write it back. Skips no-op writes, refreshes the manager cache for open siblings, and collects per-file failures instead of aborting. Fires an onFileWritten hook so the file watcher treats each write as a self-write (no reload/save storm). - Route integration updates and project rename through the propagator so closed siblings stay consistent; web falls back to the cache-only / single-file paths. - Expose getOriginalProject/updateOriginalProject on the platform manager interface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…us bar Chunk 5 of single-notebook migration (§7). - Tree is grouped: ProjectGroup (by project id) -> ProjectFile -> Notebook. A single-notebook file is a leaf labelled with its notebook; legacy multi-notebook files stay collapsible. The init notebook is excluded from counts everywhere. - Refresh is grouping-safe: refreshNotebook evicts every sibling cache entry for a project id and all refreshes fire a full-tree change (no per-item fires). - Commands are project-scoped vs notebook-scoped; new/duplicate/add-notebook create sibling files via the factory (never appended), delete removes the file for a single-notebook file, and notebook names are unique within a project group. - Add a status bar item showing the active Deepnote notebook with a "Copy Active Deepnote Notebook Details" command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
Chunk 6 of single-notebook migration (§8). - Key the server starter maps, the config handle, and the kernel auto-selector by notebook.uri.toString() - the same identity the kernel and controller use - so a notebook's server is 1:1 with its kernel. Sibling notebooks of one project no longer share a server; the working directory and SQL env are taken from each notebook's own file. - Fix environment deletion: stop every server using the environment (including closed notebooks whose server is still running) before removing the mappings, driven from the notebook->environment mapper. Drop the dead environmentServers map that was never populated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…le reader Chunk 7a of single-notebook migration (§9). - Write snapshots with notebook-scoped filenames via @deepnote/convert (generateSnapshotFilename / parseSnapshotFilename), replacing the local slug and filename regex. - readSnapshot resolves snapshots path-free (it runs at deserialize, which has no URI): glob by project id, rank the notebook-scoped match first and keep legacy project-scoped snapshots as a fallback, and skip an empty-output "latest" (save race) or a corrupt file while walking candidates. Legacy snapshots are read, never migrated or deleted. - Defer the execution snapshot save until outputs settle (quiet window with a max wait) and cancel it on re-execute / close. - Use convert's computeSnapshotHash on the save path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
Chunk 7b of single-notebook migration (§10). - The init runner now subscribes to kernel start and restart events and runs the init notebook found in its own sibling .deepnote file (matched by project id + initNotebookId via isValidSiblingInitCandidate), instead of looking it up in the main file's notebooks. - Track "init has run" per kernel in a WeakSet<IKernel>: a fresh kernel runs init once, and an in-place restart (which fires onDidRestartKernel) re-runs it so the kernel is re-initialized before the next user cell. A missing sibling is logged and skipped without permanently marking the project. - Remove the manager's persistent init-run tracking and the selector's init staging; the runner owns init triggering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #429 +/- ##
===========================
===========================
🚀 New features to boost your workflow:
|
- void the fire-and-forget onExecutionComplete call (no-floating-promises), matching the existing void performSnapshotSave pattern. - Use American "behavior" in a comment. - Add test-only technical words (basenames, initmain, Résumé, unparseable) to cspell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…of duplicating it The mocha ESM loader wholesale-mocked @deepnote/convert and reimplemented its pure helpers (resolveSnapshotNotebookId, splitByNotebooks, isValidSiblingInitCandidate, snapshot filename generate/parse, hashing, etc.). That duplicated upstream logic with no drift detection: if convert changed, the mock silently kept the old behavior and tests stayed green against a fiction. - Remove the @deepnote/convert interception from build/mocha-esm-loader.js so unit tests exercise the real package's pure functions (and now track its actual API). - Mock only the one genuinely side-effecting export, convertIpynbFilesToDeepnoteFile (real node:fs I/O), via esmock in the explorer import suites where it is used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…r paths From the Codex review of the PR (F1/F3/F4/F5), all independently verified: - F1 (P1): snapshot save fetched the cached project with getAnyProjectEntry(projectId), which can return the wrong sibling when multiple single-notebook siblings of one project are open, silently skipping the snapshot write. Use the exact getOriginalProject(projectId, notebookId) lookup instead. - F3: collectNotebookNamesForProject globbed **/*.deepnote without skipping snapshot sidecars, so stale snapshot notebook names polluted the name-uniqueness set. Filter snapshot files (matching the tree provider and propagator). - F4: detectContentChanges compared notebooks[0]; for a legacy [init, main] file the edited notebook is not at index 0, so edits were missed and modifiedAt preserved. Match the notebook by id. - F5: the deferred-save timer fired performSnapshotSave as a floating promise; wrap the save body in try/catch/finally so a build/write failure is logged (not an unhandled rejection) and execution state is always cleared. Adds regression tests for F1 (exact lookup), F3 (snapshot exclusion), and F4 (match by id). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…el init runs Addresses round-2 code-review findings G2 and G3 (both verified P2). - G2: deepnoteFileChangeWatcher's snapshot block-id recovery used the project-only getAnyProjectEntry(projectId), which can return a different open sibling's cached project (siblings share project.id), leaving originalBlocks undefined and silently skipping recovered outputs. Use the exact getOriginalProject(projectId, notebookId) — the same fix already applied to snapshotService (F1), here in the watcher path that was missed. - G3: moving init execution to the event-driven runner dropped the notebook-close cancellation that the kernel auto-selector used to provide, so closing a notebook mid-init left the remaining init blocks executing against a closed notebook. Tie the init run to a CancellationTokenSource cancelled on notebook close and dispose it in a finally. Adds regression tests for both (each fails on the pre-fix code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URccsVKXeNKZqqPi89L4ro
…ort/delete Removes two pieces of functionality; also folds in the branch's in-progress updates this work was layered on top of (they could not be isolated, as the removals are interleaved with and built on top of that WIP). Removed - project-metadata propagator: - Delete DeepnoteProjectMetadataPropagator and its types, drop the DI binding, and unwire it everywhere (activation, file-change watcher self-write hook, integration webview, explorer rename). Project-level fields are no longer fanned out across sibling files: each notebook owns its own integrations, and project-name drift is accepted for now. Drop the now-dead updateOriginalProject manager method and two stale comments. Removed - project-level explorer commands: - Delete the exportProject and deleteProject commands (constants, command-arg type, registrations, package.json command defs + sidebar menus, nls titles, and their unit tests). Per-notebook export remains via the existing exportNotebook command (first non-init notebook of the file). Also includes the branch's pending updates the above was built on: dependency bumps (incl. @deepnote/convert 4.0), the getOriginalProject -> getProjectForNotebook manager rename and getAnyProjectEntry removal, and assorted snapshot/serializer/kernel adjustments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Brings in #432 (Cloud SQL integration support). Resolved the package.json and package-lock.json conflicts by keeping this branch's newer @deepnote/* versions (blocks 4.6.0, convert 4.0.0, runtime-core 0.4.0); @deepnote/database-integrations is 1.5.0 on both sides, and the Cloud SQL source from #432 merged cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
@deepnote/blocks@4.6.0+ renders `text-cell-bullet` blocks with `indent_level >= 1` using leading spaces (two per level) before the bullet marker. stripMarkdown's bullet regex only matches at column 0, so the leading indentation must be trimmed first for the plain-text cell value to round-trip correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
…es re-exports snapshotFiles.ts re-exported six snapshot-filename helpers from @deepnote/convert. Remove the re-export block and import the helpers directly from @deepnote/convert at each use site (snapshotService.ts and the snapshotFiles unit test). snapshotFiles.ts now keeps only its local helpers (SNAPSHOT_FILE_SUFFIX, isSnapshotFile, extractProjectIdFromSnapshotUri) plus the single internal use of parseSnapshotFilename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Refactor the buildSnapshotPath method to accept an object as an argument, improving readability and maintainability. Update all relevant calls to this method throughout the snapshotService and its unit tests to match the new signature. This change enhances the clarity of parameter usage and reduces the risk of errors when passing arguments.
Trim the single-notebook test suites by removing duplicate and tautological tests and collapsing/merging several others, shrinking the PR's test additions by ~640 lines with no loss of real coverage. Cuts target only tests this branch added: - exact-(projectId, notebookId)-lookup restatements duplicated across the watcher, serializer, snapshot, and manager suites - wrapper tests already covered by the delegate's own tests (addNotebookToProject, sibling-file allocation, project-id resolution) - tautologies over trivial template/getter functions (serverUtils) - framework-registration smoke tests (status bar) Merges keep the one meaningful assertion and drop the duplicate scaffolding (e.g. legacy-delete no-op folded into the existing delete test; two init builders parametrized into one). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
When splitting a legacy multi-notebook .deepnote file into single-notebook
siblings, rename the original to `<name>.deepnote.legacy` instead of moving it
to the OS trash. The `.legacy` suffix takes it out of the extension's view (it
no longer matches `*.deepnote`) while keeping it on disk next to the split
results, so the user can restore it by removing the suffix.
Unlike `workspace.fs.delete({ useTrash: true })`, this is deterministic and does
not depend on an OS trash backend (which can be absent on headless Linux).
Collisions bump the name to `.legacy-2`, `.legacy-3`, … and the rename still
happens only after every child is durably written (write-before-retire).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test that drives the real VS Code UI through the on-open split of a legacy multi-notebook .deepnote file: it asserts the split prompt, the one-file-per-notebook result, the retained `.legacy` backup, that each sibling opens without re-prompting, and that content plus the project integration fan out into every split file. Add a `createScreenshotter(this)` helper that captures step screenshots into a per-spec directory derived from the running test file (`test/e2e/screenshots/<spec>/`), plus the `sales-analytics.deepnote` fixture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
…ites Add ExTester end-to-end suites covering: - opening a plain single-notebook file (opens directly, no split prompt, the status bar shows the notebook name); - splitting a multi-notebook file that declares an init notebook (the init notebook becomes its own single-notebook sibling; each main sibling still references it via initNotebookId); - the init-notebook runner: the sibling init notebook runs hidden in a main notebook's kernel so its definitions are available, and re-runs after a kernel restart. Add the quick-notes and etl-pipeline fixtures (including the pre-split extract/init siblings), and disable the kernel-restart confirmation in the E2E settings so the restart test can drive it non-interactively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test asserting the Deepnote Explorer groups sibling
.deepnote files by project: three files sharing one project.id collapse into a
single "Marketing" group ("3 files") whose leaves are the three notebooks, while
a file from a different project appears as its own group. Reads the tree by
diffing visible leaves before/after expanding (avoids the page-object library's
flaky CustomTreeItem.getChildItems). Adds the three marketing fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
When several E2E suites run in one ExTester session (as in CI, via the `*.e2e.test.js` glob), every workspace-folder open after the first failed with "Failed to open folder after 5 attempts" in the suite's `before all` hook — only the alphabetically-first suite passed. Root cause: the simple "Open Folder" dialog (files.simpleDialog.enable) navigates one directory level *toward* the typed path per OK click and only accepts the folder once the browser is AT it. The helper clicked OK once then re-opened the dialog each attempt, which reset navigation back to the default directory — for the 2nd+ open that default is the previous, now-deleted workspace, so the dialog fell back to "/" and never converged on the target. Fix: click OK repeatedly within a single dialog until the pre-open workbench element detaches (reload = folder accepted), instead of re-opening per attempt; and set `window.openFoldersInNewWindow: "off"` so "Open Folder" reuses the current window, keeping that reload detectable. Verified with four suites (16 tests) opening four folders in one session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test for the Deepnote status-bar item: it shows the active notebook's name (with the "Copy Active Deepnote Notebook Details" tooltip), hides when a non-notebook editor is focused, and — on click — copies the notebook details to the clipboard with a confirmation toast. The clipboard is verified by pasting into a scratch text file and reading it back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test for the notebook-management commands that create and rename sibling .deepnote files from the Deepnote explorer: New Notebook, Add Notebook (project-group context menu), Duplicate Notebook, and Rename Notebook — each verified by the resulting notebook name inside the sibling files plus the confirmation toast. Delete Notebook is included as a pending test: its context-menu -> native confirmation-modal interaction is unreliable to drive under ExTester (documented inline), so it is left as a manual check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test for the Deepnote integrations UI: opening "Manage Integrations" for a notebook whose project declares an integration lists it (the "Sales BigQuery" integration on the sales-analytics-revenue fixture), while a plain notebook (quick-notes) shows no such integration. Adds the sales-analytics-revenue fixture (a single-notebook split of the Sales Analytics project carrying the BigQuery integration + its SQL cell). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
Add an ExTester end-to-end test for renaming a Deepnote project from the Explorer: none of the three "Marketing" siblings is opened, then the project group is renamed to "Growth" via its context menu; the new name is asserted to fan out to every sibling .deepnote file on disk (and the old name gone), with the Explorer group relabelled and a confirmation toast. Extract the shared Deepnote tree helpers (getDeepnoteExplorerSection, readDeepnoteTreeRows, findDeepnoteGroup/Leaf, selectDeepnoteContextMenu) into test/e2e/helpers/deepnoteTree.ts for reuse across the tree-driven suites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2CLu8UmD8ceGNyv8u96pQ
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/e2e/suite/projectRename.e2e.test.ts (1)
51-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winName the editor-focus delay.
Extract
400to a module-levelEDITOR_FOCUS_SETTLE_DELAYconstant.Proposed change
const TREE_LOAD_TIMEOUT = 30_000; +const EDITOR_FOCUS_SETTLE_DELAY = 400; - await driver.sleep(400); + await driver.sleep(EDITOR_FOCUS_SETTLE_DELAY);As per coding guidelines, “Extract magic numbers (retry counts, delays, timeouts) as named constants near the top of the module.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/e2e/suite/projectRename.e2e.test.ts` at line 51, Extract the 400 millisecond delay used by the editor-focus flow in projectRename.e2e.test.ts into a module-level constant named EDITOR_FOCUS_SETTLE_DELAY near the top of the module, then use that constant in the driver.sleep call.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@test/e2e/suite/projectRename.e2e.test.ts`:
- Line 51: Extract the 400 millisecond delay used by the editor-focus flow in
projectRename.e2e.test.ts into a module-level constant named
EDITOR_FOCUS_SETTLE_DELAY near the top of the module, then use that constant in
the driver.sleep call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 58b71a30-0d25-4305-ab74-8b6030cf99f8
📒 Files selected for processing (1)
test/e2e/suite/projectRename.e2e.test.ts
… files The `notebook` codicon renders as a clock face at tree size (verified against the codicon set in VS Code 1.111 — it's a real glyph, `\ebaf`, not a fallback), so every `.deepnote` file showed a clock in the Explorer. Replace it: - Single-notebook files now use `file-code`, matching the notebook children they represent — every "notebook you open" looks the same. - Legacy multi-notebook files use `book` (a container of notebooks), and their tooltip gains a "(legacy)" suffix to mark the retired one-file-many-notebooks layout. Icons rendered and confirmed in a headless VS Code 1.111 (folder / book / file-code, no clock); tree-item and provider unit-test icon and tooltip assertions updated accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
|
@dinohamzic thanks for the findings .. I've addressed all of them |
dinohamzic
left a comment
There was a problem hiding this comment.
I didn't spot any major issue while manual testing, and the reported ones are fixed, nice one.
Another AI review in case you spot anything relevant:
-
[P1] Integration updates can report success without reaching disk. projectIntegrationsWriter.ts:84
cacheUpdatedmakes the operation succeed even when no files were written—for example, when a.deepnotefile is opened outside a workspace folder—or when some sibling writes fail. The webview then posts “Configuration saved successfully,” but integration membership can disappear on reopen or differ across siblings. Persistence should target the active file explicitly and report/throw on incomplete fan-out. -
[P2] Snapshot selection is still capped before ranking. snapshotService.ts:909
The scoped search still returns an arbitrary 200 files before sorting, so after 200 executions the notebook’s
latestor newest snapshot can be omitted. The legacy pass is broader and filters sibling-scoped files only after the same cap, so noisy siblings can still hide the legacy fallback. Query exactlatestpatterns first, then apply retention or a cap after filtering and ordering. -
[P2] A failed child write can still leave an orphan. deepnoteMultiNotebookSplitter.ts:155
Cleanup is registered only after
writeFileresolves. Filesystem providers may create or partially write the destination before rejecting, leaving an untracked sibling that causes suffixed duplicates on retry. Register safe cleanup before the write or explicitly probe/delete the current target on failure. -
[P2] Splitting proceeds when the original tab failed to close. deepnoteMultiNotebookSplitter.ts:261
tabGroups.close()returns whether the close succeeded, but the result is ignored. If it returns false, the extension renames the file while its editor remains open; a later save can recreate the legacy.deepnotefile alongside the generated siblings. Return/check the boolean and abort before rename.
Saving integrations reported success from the in-memory cache update alone, so a
folderless / out-of-workspace notebook (findFiles covers only open folders) or a
partial sibling fan-out could post "Configuration saved successfully" having
written nothing to the file being edited — integration membership then vanished
on reopen.
persistProjectIntegrations now writes the ACTIVE file explicitly first, then fans
out, and returns { activePersisted, siblingsFailed } judged from disk (not the
cache); the webview only claims success when the active file reached disk and
warns on an incomplete fan-out. persistProjectIntegrations / writeIntegrationsToFile
also take a single params object instead of positional arguments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
findSnapshotOutputs passed maxResults to findFiles, which truncates an UNORDERED set to 200 BEFORE ranking — so a notebook with >200 of its own snapshots could have its `latest` dropped (stale outputs on open), and a sibling flood could push a notebook's legacy fallback out of the window (no outputs on open). The glob is now uncapped and the 200 bound is applied to the ranked candidate list, so the best candidate is always considered and only the read loop is bounded. Tests honor findFiles' maxResults so a pre-ranking cap is observable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
…en tabs Two failure paths after the child-write phase of a split: - A provider that created a destination then rejected mid-write left an untracked orphan, because the delete-compensation was registered AFTER the write; a retry then bumped the name to a "-2" duplicate. Register the cleanup BEFORE the write, guarded by an exists() probe so a write that created nothing stays a no-op. - closeNotebookTab discarded the tabGroups.close() result, so a re-dirtied tab whose close was cancelled left the file renamed out from under an open editor — a later save recreated the legacy .deepnote. It now returns the boolean (and closes every matching tab); the split throws before rename if it can't close. Adds a TabInputNotebook mock (verified field/constructor-exact against the real vscode 1.95.0 source) so the close-cancelled path can be driven in a unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
"folderless" is flagged by cspell; reword to "no open folder" instead of adding a dictionary entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/notebooks/deepnote/integrations/projectIntegrationsWriter.ts`:
- Line 69: Update the project integration discovery flow around the findFiles
call and its returned activePersisted/siblingsFailed result to track discovery
failures separately. Propagate that failure through the result so the webview
can warn that sibling propagation may be incomplete, rather than reporting full
success when a workspace folder scan fails.
- Around line 94-97: After re-reading the project in the save flow, validate
that projectData.project.id still matches projectId before assigning
integrations in projectIntegrationsWriter. If it does not match, stop without
writing; preserve the existing integrations-only update for the matching
project.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0a76c43a-f082-41dd-8d9b-261de2f5f984
📒 Files selected for processing (12)
src/notebooks/deepnote/deepnoteMultiNotebookSplitter.tssrc/notebooks/deepnote/deepnoteMultiNotebookSplitter.unit.test.tssrc/notebooks/deepnote/integrations/integrationManager.tssrc/notebooks/deepnote/integrations/integrationWebview.tssrc/notebooks/deepnote/integrations/integrationWebview.unit.test.tssrc/notebooks/deepnote/integrations/projectIntegrationsWriter.tssrc/notebooks/deepnote/integrations/projectIntegrationsWriter.unit.test.tssrc/notebooks/deepnote/integrations/types.tssrc/notebooks/deepnote/snapshots/snapshotService.tssrc/notebooks/deepnote/snapshots/snapshotService.unit.test.tssrc/test/mocks/vsc/index.tssrc/test/vscode-mock.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- src/notebooks/deepnote/integrations/integrationManager.ts
- src/notebooks/deepnote/integrations/integrationWebview.unit.test.ts
- src/notebooks/deepnote/deepnoteMultiNotebookSplitter.ts
- src/notebooks/deepnote/deepnoteMultiNotebookSplitter.unit.test.ts
- src/notebooks/deepnote/snapshots/snapshotService.unit.test.ts
- src/notebooks/deepnote/snapshots/snapshotService.ts
The integration writer checked project.id before flushing the open document, but the flush can save a stale dirty tab whose on-disk file was swapped to a different project. The post-flush re-read was then written without re-checking identity, so integrations could land in the wrong project's file. Re-validate after the re-read and skip on mismatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
Resolves conflicts from the single-notebook refactor (#429) against the telemetry PR: - Explorer/environments/integration/activation constructors: keep both sides' injected params (added `analytics: ITelemetryService`). - DeepnoteExplorerView adopts main's refactored command handlers and re-applies telemetry: methods return `boolean` so the command wrappers record `{ completed }` (open/create/import/delete/duplicate/export). - Dropped `delete_project` and project-level `export_notebook` telemetry: #429 removed the deleteProject/exportProject commands, methods, and package.json menus entirely. Removed the now-unused `delete_project` from the TelemetryEventName union. - deepnoteCellExecutionAnalytics: use main's `getProjectForNotebook(projectId, notebookId)` (was `getOriginalProject`). - package-lock.json: main's lockfile + posthog-node@4.18.0. typecheck 0, lint 0, compile-tsc 0, unit tests 2502 passing / 0 failing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
Brings in the squash-merged PR #429 ("one notebook per .deepnote file"). This branch forked from an earlier tk/single-notebook, so #429's later work surfaced as conflicts in shared files. Resolved by taking #429's newer versions of the shared single-notebook code (persistProjectIntegrations refactor with activeFileUri, snapshot candidate ranking, abortable tab-close) while preserving this PR's env-file integration additions and unioning divergent imports. Compiles clean; full unit suite green (2543 passing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CPTs6CHNncauUuTGkwpNtH
Collapse the six processService.exec call sites into a single runPython helper that forwards the token and re-checks it afterwards, so a new exec cannot silently omit either. Removes seven now-redundant throwIfCanceled calls and three restatements of the same rationale comment. Deduplicate the two identical kernel-spec try/catch blocks into tryInstallKernelSpec; mutation testing showed one of the two cancellation guards was covered by no test at all. Add resolvePythonExecutable to the runtime-core test mock. Merging main brought in the ESM loader interception from #429, which lacks that export, so loading the installer aborted the whole unit suite at import time. Extend the cancellation tests from 3 to 9: the existing ones only asserted token forwarding, leaving the cancellation outcomes untested. Each new test was verified to fail when its fix is reverted. Drop the `ex as Error` casts (useUnknownInCatchVariables is off, so they were no-ops) and trim comments that restated the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k
* fix: forward cancellation token to toolkit installer subprocesses The pip/venv exec calls in the Deepnote toolkit installer were started without the CancellationToken, so cancellation was only checked between calls. Cancelling during a multi-minute pip install did nothing until the install finished, leaving the Stop button unresponsive. Pass the token into every processService.exec call (and thread it through isToolkitInstalled) so cancelling now terminates the running subprocess immediately. * fix: handle cancellation outcomes now that execs are killable ProcessService.exec resolves with partial output when the token kills the process (only shellExec rejects), so forwarding the token exposed several paths that misread a cancelled exec as a domain result: - rethrow CancellationError unwrapped from installVenvAndToolkit's catch so upstream isCancellationError checks suppress the error UI instead of showing an install failure - make isToolkitInstalled cancellation-aware: throw on cancel after the probe exec instead of returning undefined, which misdiagnosed healthy venvs as toolkit-missing and successful installs as failed verification - require the token parameter on isToolkitInstalled so future callers cannot silently reintroduce an uncancellable probe - check for kernel.json rather than the kernelspec directory and re-check the token after the ipykernel exec, so a cancelled install cannot leave a permanently trusted partial kernelspec - re-check the token in installAdditionalPackages before reporting success, and log cancellation instead of a failure message Rewrite the unit test on the repo's ts-mockito pattern (capture/verify, deepStrictEqual per CLAUDE.md) and cover the ensureVenvAndToolkit probe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: route toolkit installer execs through one cancellable helper Collapse the six processService.exec call sites into a single runPython helper that forwards the token and re-checks it afterwards, so a new exec cannot silently omit either. Removes seven now-redundant throwIfCanceled calls and three restatements of the same rationale comment. Deduplicate the two identical kernel-spec try/catch blocks into tryInstallKernelSpec; mutation testing showed one of the two cancellation guards was covered by no test at all. Add resolvePythonExecutable to the runtime-core test mock. Merging main brought in the ESM loader interception from #429, which lacks that export, so loading the installer aborted the whole unit suite at import time. Extend the cancellation tests from 3 to 9: the existing ones only asserted token forwarding, leaving the cancellation outcomes untested. Each new test was verified to fail when its fix is reverted. Drop the `ex as Error` casts (useUnknownInCatchVariables is off, so they were no-ops) and trim comments that restated the code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k * test: use US spelling in a test name to satisfy cspell cspell.json sets "language": "en", so the en-US dictionary rejected "recognised" in a test title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k * fix: reap the whole subprocess tree when an installer exec is cancelled Forwarding the token made the installer's subprocesses killable, but ProcessService.kill only signals the process we spawned. `python -m venv` runs ensurepip in a subprocess and pip shells out to build backends, so cancelling left survivors writing into the venv that the retry path is busy deleting recursively. exec now opts into its own process group when the caller asks for `detached`, and kills that group instead of the single pid. Windows is unchanged by construction: taskkill /T already reaps the tree, and `detached` there pops a console window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k * fix: stop reporting a cancelled toolkit install as a failure Now that the execs are actually killable, cancelling surfaces immediately instead of after pip has finished anyway, so the unguarded catches around the install and kernel-selection flows raise "Failed to install deepnote-toolkit: Canceled" and "Failed to load Deepnote kernel: Canceled" on every Stop. Neither is a failure the user needs to see. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k * style: apply new-file conventions to the proc cancellation test Drop the Microsoft copyright header and move the vscode-path import into the local-import group, matching every other file this branch adds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeHawfLMtvXn252EkCip2k * Fix spell check --------- Co-authored-by: Marko Paleka <slightly@Markos-MacBook-Air.local> Co-authored-by: Jakub Jurových <jakub.jurovych@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: tomas <tomas@kislan.sk> Co-authored-by: Dino Hamzić <dino@subtlebits.com>
Resolves 4 conflicting files. The branch's interpreter-based direction is kept; main's changes to the same code are carried forward: deepnoteServerStarter.node.ts - Keep IInstaller/Product.deepnoteToolkit and deriveEnvPath (branch); take applyIntegrationEndpointEnv over the deleted gatherSqlIntegrationEnvVars (#440). deepnoteKernelAutoSelector.node.ts - Drop onKernelStarted/init staging: IDeepnoteInitNotebookRunner no longer exists, the runner subscribes to kernel start itself (#429). - getOriginalProject -> getProjectForNotebook (#429). - Collapse baseFileUri/projectKey/notebookKey to getNotebookKey() (#429). - Replace the local projectServerHandles map with IServerHandleRegistry, keeping #429's lifecycle: the old handle is unregistered only after a successful switch. - Restore the controller identity check (#358) and findNotebookEditor, so notebook.selectKernel gets a NotebookEditor rather than a NotebookDocument (#430). - Restore the cancellation early-return in handleKernelSelectionError (#409). - Placeholder controllers and the select_environment telemetry call site stay removed: both existed only to drive the environment picker this branch deletes. Tests - Port main's server-handle lifecycle, controller-identity and cancellation tests; drop the environment-picker, placeholder and telemetry tests along with the code. - Stub the Python API in the DeepnoteServerStarter suite, which now reaches deriveEnvPath -> getCachedEnvironment. Verified: typecheck 0, compile 0, 2774 unit tests passing, 0 failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019cqzx43mLQRvVfWbfEUUSx
…n suites Five suites each carried their own copy of the same `workspace.fs` fake: serve `.deepnote` files by `fsPath`, reject any other read, and parse every write into a map. The writer suite's `stubWorkspace` (#429) was the original; this branch added four more copies: the picker's `stubWorkspace`, the manager suite's `setup()`, `stubProjectFileOnDisk` and `stubProjectFiles`. `stubDeepnoteFiles` in `src/test/mocks/vscodeFs.ts` now holds that core. It is built on `stubReadFile`, which now rejects a URI its resolver returns `undefined` for (its two callers always return a string). The options cover what the copies added: `failWriteFor` (writer, manager), `onRead` (picker) and `onWrite` (writer). Each suite keeps its own `findFiles`, workspace-folder and open-notebook wiring, which is where they really differ. The manager suite's failed writes now reject with "write failed for" instead of "write blocked for"; no test reads the message. The helper uses `fsPath`, which `local-rules/dont-use-fspath` bans outside `.node.ts` and `.test.ts` files; like the three `src/test/datascience` helpers, the file disables the rule. Verified: - `npm test`: 2879 passing, 239 pending, as before. In the five suites and the `stubReadFile` callers, the same 175 tests pass, by title. - `npm run typecheck`, `lint` (the same 27 existing warnings as HEAD), `format` and `spell-check` exit 0. - The suites go through the helper: dropping the write capture fails 19 tests across all five suites; ignoring `failWriteFor` fails 3 writer and 2 manager tests; reading it only at stub time fails the 2 manager tests that add a failure after setup; dropping `onWrite` fails the writer's cache-ordering test; dropping `onRead` fails the picker's 2 mid-scan cancellation tests. - Not pinned by a test: serving an unknown path as an empty file instead of rejecting passes everything, because the code under test treats an unparsable file like an unreadable one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv
… integrations across projects (#516) * feat(integrations): add an "Add Existing Integration" picker to reuse integrations across projects Integrations created through the extension UI are stored in SecretStorage keyed by integration id, but each project's `.deepnote` roster decides which of them apply to it, so the same database had to be configured again for every project in a workspace. Add `deepnote.addExistingIntegration` (command palette, and an "Add Existing Integration" button in the Manage Integrations panel). It scans the workspace's `.deepnote` files for integrations other projects declare that have a stored config, offers them in a QuickPick (name, type, which projects use them), and links the chosen one into the active project's roster through `persistProjectIntegrations`. Credentials are not copied: the roster entry is the only per-project scoping, so the linked project resolves the same config (and federated refresh token). Running kernels of the project get an env refresh and the panel is re-shown, since no storage change event fires for a roster-only edit. Integrations already on the roster, file-only (`.deepnote.env.yaml`) ones and ids whose roster type disagrees with the stored config are not offered; the last case is reported with a warning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(integrations): keep the full project roster when attaching an existing integration `getCachedRoster` narrowed the cached roster to the types the panel can manage and `attachExistingIntegration` persisted that narrowed array, so linking an integration silently dropped `pandas-dataframe` (and any type this build does not know) from the project's integrations. The roster now passes through verbatim, following the same cast-not-narrow pattern `SqlCellStatusBarProvider.addToProjectIntegrations` already uses, and only the picker's exclusion set is derived from it. Adds a picker test and a manager test asserting a `pandas-dataframe` entry survives the attach in both the cache update and the written file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(integrations): trim comments to carry why, not what Drops comments that restated the code they sat on, condenses the docstrings that explained storage internals at tutorial length, and states the "reuse is a link, not a copy" rationale once in collectReusableIntegrations instead of in three places. Also moves add_existing_integration to its alphabetical slot in TelemetryEventProperties. It had been inserted between the "No `outcome`: ..." docblock and refresh_integration_env, so that block read as documenting an event that does carry an outcome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NjAiQLqjuPiZmN9GiTXCfd * fix(integrations): close data-loss windows in add existing integration Addresses the review of this branch. Correctness: - Re-read the project's integrations after the QuickPick instead of writing back a snapshot taken before it. The file watcher replaces the cached project on an integrations-only external write without any UI event, so the pre-pick array could be stale, and the writer stamps whatever it is given onto every .deepnote file of the project. - Refuse the command on a *.snapshot.deepnote file. Snapshots match the notebook selector and the writer skips them, so the command reported a failure only after it had already updated the cache. - getCachedProjectIntegrations returns undefined on a cache miss rather than an empty array, so "not cached" can no longer be written back as "no integrations at all". - Make the workspace scan cancellable: window.withProgress plumbs a token into collectReusableIntegrations, findFiles and both scan loops. Types: - Widen the write path to RawProjectIntegration, now defined once in notebooks/types.ts. This removes both `as ProjectIntegration[]` assertions and two duplicate local declarations of the type. Narrowing inside the writer is now a compile error rather than a silently exhaustive switch. Naming: - "roster" becomes "project integrations" throughout. In the SQL status bar the narrower and wider lists are now projectIntegrations and selectableIntegrations, matching getSelectableIntegrations. Localization: - Integration type labels were written out in seven places, one of which (the webview map) was never localized at all. They now live only in localize.Integrations.typeLabels, keyed by a bundle key derived from the integration type so there is no second list to keep in sync. Drops the dead integrationsDuckDBTypeLabel. Tests: - Cover every failure branch of addExistingIntegration, plus the stale re-read, the snapshot guard, the cache miss and both cancellation paths. Each new test was verified to fail against the unfixed code. - Type refreshSpy off IIntegrationEnvLiveRefresher so a signature change fails the compile, not just the runtime assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * fix(integrations): validate notebook, cache and scan state in add existing integration Three places where the command acted on state it had not re-derived at the moment it acted: - The panel posts the notebook URI it was opened for, and nothing clears it when that notebook closes. `resolveDeepnoteNotebook` then fell through to the focus fallbacks, so the pick was written into whichever other project happened to be visible, along with its siblings and a kernel env refresh. Reject a supplied URI that no longer resolves; the palette path passes no URI and still needs the fallback. - `persistProjectIntegrations` updates the cache before it writes, so a failed write left the link in the roster. The retry then excluded that id and reported nothing available, while the file still lacked it. Restore the pre-write roster on the failure branch. - `collectReusableIntegrations` only checked the cancellation token at loop heads, so a cancel during the last file, or during `findFiles`, returned `cancelled: false`. That opened an unwanted picker and, in a single-root workspace, claimed no integrations were available after an unfinished scan. Recheck the token before assembling the result. Each fix has a regression test that was seen to fail against the unfixed code. The picker test harness now models `findFiles` resolving empty on a tripped token, which is what let the scan bug go uncovered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * refactor(integrations): throw CancellationError from the reusable-integration scan `collectReusableIntegrations` signalled cancellation with a `cancelled` flag alongside two fields that were always empty when it was set — a sum type encoded as a product. The cost showed up as three identical dead literals and two doc comments whose only job was to warn that the other fields were unsafe to read. It also made the failure silent by default: a caller that forgets to check the flag proceeds on an empty roster and reports "no integrations available" after an unfinished scan, which is the bug fixed in the previous commit. A throw cannot be forgotten. Use the helpers the repo already has — `Cancellation.throwIfCanceled` and `isCancellationError` from `platform/common/cancellation`, as the kernel layer does — so the result type is now always meaningful and both warnings are gone. The only caller handles it in the try/catch that AGENTS.md already prescribes around `withProgress`, and rethrows anything that is not a cancellation, which is what happened before. Verified by mutation: neutralising the three guards fails exactly the three cancellation tests and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * refactor(localize): reorder imports and improve comment clarity in Integrations namespace * test(e2e): cover the add-existing-integration command end to end Drives the real reuse flow in one temp workspace holding two fixture projects: runs the command on "Quick Notes" while the sibling project only declares an integration it never configured, configures a PostgreSQL integration through the integrations panel, then reuses it from "Sales Analytics". Seeding goes through the panel because the picker offers only ids with a config in SecretStorage, and the panel's Save is its one writer. A `.deepnote.env.yaml` cannot stand in: file-only integrations are excluded from the picker by design, since they already apply to every project under the file. Asserts the rules a unit test cannot reach end to end: - a declaration without stored credentials is not offered - the picked row carries the name, the type label and "Used in: <project>" - the target project's file gains the link and KEEPS the integration it already declared - the credentials never reach that file Both halves were verified to fail against mutated builds: dropping the existing entries in attachExistingIntegration fails the roster assertion, and offering entries with no stored config fails the "nothing to reuse" case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * test(e2e): collapse the add-existing-integration assertions into one test The five `it()` blocks shared a single `before()` hook and only read state it had already captured, so the split bought separate mocha records and nothing else: the hook takes ~35s, all five assertion bodies together took 296ms in CI. Each former test name stays as a section comment, and the `expect` message strings still name every assertion in failure output. The tradeoff is fail-fast: mocha now stops at the first broken expectation instead of reporting the others alongside it. Verified by running the suite headlessly — 1 passing (35s), with all four scenario screenshots produced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * refactor(integrations): merge an added integration into each file on write The writer replaced `project.integrations` wholesale with an array its caller had built, so both callers that only wanted to append one entry had to do their own read-modify-write and own the freshness of that snapshot. `addExistingIntegration` re-read the cache after the pick to keep its snapshot fresh, which narrowed the window rather than closing it: four awaits still sat between that read and `workspace.fs.writeFile`, and anything added to the project in between was stamped away — from every `.deepnote` file of the project at once. `writeIntegrationsToFile` already re-reads each file from disk, so it now builds the array to write from that read via a resolver. `persistProjectIntegrations` passes `() => integrations` and is unchanged byte for byte, so the panel's replace path (how deletes happen) keeps its semantics. The new `addProjectIntegration` passes the merge, and the two append callers use it. Drops from `addExistingIntegration`: the second cache read, its guard, and the cache rollback. `getCachedProjectIntegrations` is now only the scan's exclusion set, where staleness is harmless. Two behavior changes: - The base is the file, not a caller snapshot, so the read and the write are adjacent with only the merge between them. Siblings that had already diverged keep their own entries instead of being flattened. - The cache moves only once the active file is on disk. Previously it moved first; `addExistingIntegration` rolled it back on failure while `sqlCellStatusBarProvider` did not, leaving the cache holding an integration that never reached disk. The new regression test was verified to fail against a mutated writer that ignores what the file holds: it wrote [pg-shared] instead of [added-meanwhile, pg-shared]. The SQL status bar test previously stubbed no filesystem at all and passed because of the bug above; it now has a readable project file and asserts the entry reaches disk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * fix(integrations): move the cache update ahead of the sibling sweep f82d19741 moved the cache update to after the whole sibling sweep, which is the slow half of the write: findFiles plus a read and a write per file. Saving a notebook rebuilds its entire `.deepnote` from the cached project (`deepnoteSerializer` reads the cache, stores it back, then serializes it), so a save landing inside that window wrote the pre-add roster straight back over the active file, and the watcher reloaded the cache from that. End state: siblings carried the link, the file the user was working in did not, and the success toast had already been shown. Split the sweep out of `writeProjectFiles` so the cache moves between the active write and the siblings. Both invariants hold: nothing reaches the cache that did not reach the disk, and the write is the last await before the cache moves. The sweep still runs when the active write fails, as before. The new test asserts the order is active-write, cache, sibling-write; it was verified to fail against f82d19741's ordering and nothing else did. Also: the doc comment on `writeIntegrationsToFile` still described the string outcomes it stopped returning, the log prefixes still said `persistProjectIntegrations` on lines the new entry point also reaches, and one test message claimed nothing reached disk in a case that now writes the file before the cache throws. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * fix(sql): refuse to switch the integration of a snapshot notebook `*.snapshot.deepnote` matches the notebook selector, so the SQL cell's integration picker is reachable on a snapshot. Switching there edited the record's own cell metadata and, through `addToProjectIntegrations`, rewrote the real project files behind it — the snapshot's active write is skipped, but the sibling sweep still stamps every matching `.deepnote`. `addExistingIntegration` has guarded this since 058fe8ffa; this path never did. Guard at the top of `switchIntegration` rather than inside `addToProjectIntegrations`, so the cell metadata edit is refused too. The test arranges the project, the pick and the edit so the switch would otherwise go through, leaving the snapshot check as the only thing that can stop it; it was verified to fail with the guard disabled. An earlier version of it passed with the guard disabled, because an unstubbed project sent it down the "Project not found" return instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * test(integrations): drop duplicated manager tests, pin the error each branch shows Three tests in the addExistingIntegration suite asserted claims that already live in `projectIntegrationsWriter`/`existingIntegrationPicker`, and one assertion in them could not fail at all: - the pandas-dataframe case is the picker's "passes entries of types it cannot manage through verbatim" one level up. Its non-duplicate assertion — that DuckDB is "neither offered nor a candidate" — holds because `storedConfigs` has no entry for that id, so it still passed with both the self-project skip and the configurable-type filter removed. - "leaves the cache alone when the active file cannot be written" is `projectIntegrationsWriter.unit.test.ts`'s test of the same name; the ordering it pins is `addProjectIntegration`'s alone. The claim moves into the `lateFailures` body, where it holds for both rows. - the two mid-pick tests drive one path. The fs stub reads `onDiskCurrent ?? currentProject`, so moving the cache moves disk too, and the cache is read once at the top of the command, before the scan — a mutation during the pick is unobservable. The disk-only test is the stronger of the two and stays. The two standalone early-failure tests assert a subset of the table that followed them, so they become rows in it. `showErrorMessage(anything())` let every early failure claim any diagnosis: pointing the snapshot branch, and then the notebook-closed branch, at the generic failure text passed the whole suite. Each row now names the message it expects, and both mutations were seen to fail. 18 tests -> 15. Verified the rest of the coverage survives: the merge, cache-ordering, snapshot-guard and stale-panel mutations each still fail the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * fix(integrations): do not sweep siblings when the active write did not land `addProjectIntegration` swept every sibling `.deepnote` whatever the active file returned. `writeIntegrationsToFile` has four ways not to write: a snapshot, a file whose project id does not match, one whose unsaved edits cannot be flushed, and a rejected write. In all four the sweep still stamped the integration across the project's real files on behalf of a file that never took it. The snapshot case is the one that has already cost two fixes — 058fe8ffa and ab1a81e8d both guard at a caller, so the next caller to reach this function gets no protection. `failed` is the milder half: the siblings end up carrying a link the active file and the cache do not, while the user is told the add failed. Return before the sweep instead, so the active file is the single mandate for touching anything. `siblingsFailed: 0` on that return is safe: both callers read it only when `activePersisted` is true. The regression tests cover both halves — a refused active write and a skipped snapshot, each with a sibling on disk — and were seen to fail against the old code on `writes.size`, with the sibling written. `persistProjectIntegrations` keeps its own shape: it updates the cache before the active write by design, so the same early return there is a larger change and is left for its own commit. Also carries a rename of `active` to `writeOutcome` that was already in the tree, on the lines this touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ns2d146xs51a6E8LeDgJQS * refactor(integrations): narrow the scan's catch variable, not assert it `addExistingIntegration` passed the scan's catch variable to `isCancellationError` as `error as Error` (added in d3ea64c9a). With `useUnknownInCatchVariables` off in tsconfig.base.json, `error` is `any`, so the assertion claims an `Error` without checking for one. It would also keep compiling, and mask `unknown`, if that flag were turned on. `error instanceof Error && isCancellationError(error)` checks what the assertion only claimed. It gives the call the same static type, stays correct under `useUnknownInCatchVariables`, and matches the existing idiom in deepnoteKernelAutoSelector.node.ts. The scan throws nothing but the `CancellationError` from `Cancellation.throwIfCanceled`, so every outcome it can produce is unchanged: a cancellation still returns `cancelled`, and anything else is still rethrown. No test is added. HEAD and this change differ only for a non-`Error` or cross-realm value that looks like a cancellation, which the scan cannot throw, so a test pinning that difference would only detect the change. Verified: - npm run typecheck: exit 0 - npm run lint: exit 0, no findings in integrationManager.ts - npx prettier --write / --check on integrationManager.ts: no changes, check passes - npm run compile-tsc: exit 0 - npm run test:unittests -- --grep "IntegrationManager.addExistingIntegration": 15 passing before and after the change - grep -n "as Error" integrationManager.ts: no matches - mutation: with the guard narrowed to `instanceof TypeError`, "returns cancelled and writes nothing when the scan is cancelled" failed on the `CancellationError` rethrown from `throwIfCanceled`, so the existing test exercises the new condition; it passes again with the change restored Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): report cancellations in add_existing_integration `CommandOutcome` exists so telemetry can separate user drop-off from real failures, and `add_existing_integration` is typed with it. But `addExistingIntegration` left both of its cancel exits, a cancelled scan and a dismissed picker, before the only `trackEvent` call, so the event could only ever carry 'completed' or 'failed' and drop-off in this command was never measured. `split_notebook`, the explorer commands and `authenticate_integration` all report 'cancelled'. Both cancel exits now send the event through a private `trackAddExistingIntegration(outcome, integrationType?)`, which the final send uses too. Nothing has been picked on either cancel exit, so the type falls back to 'unknown', the placeholder `integrationWebview.ts` already uses when no type is known. The five early-failure exits and the "none available" exit stay untracked, as before. Counting "none available" as 'completed' would record completions where nothing was linked, so that needs its own product call. The two cancel tests pinned the old silence with `trackEvent(anything()).never()`. Each now asserts exactly one event, `trackEvent(anything()).once()`, and that it is the 'unknown' / 'cancelled' one, `trackEvent(deepEqual(...)).once()`. The count check is what rejects a stray second event. Verified: - npm run typecheck: exit 0 - npm run lint: exit 0, no findings in the changed files - npx prettier --write / --check on both files: no changes, check passes; npm run format: exit 0 - npm run compile-tsc, then npm run test:unittests -- --grep "IntegrationManager.addExistingIntegration": 15 passing. Against the unfixed manager, both updated tests failed with `Expected "trackEvent(anything())" to be called 1 time(s). But has been called 0 time(s).` - Mutations, each recompiled and run, then edited back: - a stray 'failed' event after the dismissal's 'cancelled' one: the picker test fails, trackEvent(anything()) called 2 time(s) - the dismissal tracked twice: the picker test fails the same way - the dismissal tracked as 'completed': the picker test fails, trackEvent(deepEqual(...)) called 0 time(s) - the scan cancel tracked as 'failed': the scan test fails, trackEvent(deepEqual(...)) called 0 time(s) - npm test: 2864 passing, 239 pending, 0 failing Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): re-show the panel before the kernel env refresh After a link lands, `addExistingIntegration` shows the success toast and awaits `refreshAfterProjectIntegrationsChange`, which awaited the kernel env refresh before it re-showed the panel. The refresh runs `executeHidden` in every running kernel of the project, and a kernel busy with a running cell answers only after that cell finishes. Until then the panel kept its pre-link list, and a save, reset or delete from it rewrote the cache and every project file from that list, silently dropping the link the user had just been told was added. With no panel open, the panel opened only once the kernels were free. The panel is now re-shown first and the kernels are refreshed after. Each call keeps its own try/catch, so a failed re-show still refreshes the kernels, and both stay awaited. Step 4 of "Add Existing Integration" in specs/INTEGRATIONS_CREDENTIALS.md now gives the order and the reason for it. By reading, this narrows the window but does not close it: the panel is still on the old list while the writer sweeps the project's sibling files, after the cache update and before the re-show. That wait is bounded by local I/O, and its cause, the panel writing back a snapshot that replaces the whole list, predates this branch. Verified: - npm run typecheck: exit 0 - npm run lint: exit 0, no findings in the changed files - npx prettier --write / --check on the three files: no changes, check passes - npm run compile-tsc, then npm run test:unittests -- --grep "IntegrationManager.addExistingIntegration": 15 passing before, 17 passing after. Against the unfixed order, 're-shows the panel before the kernel env refresh settles' failed with `Expected "show(strictEqual(project-current), anything(), anything(), anything(), anything())" to be called 1 time(s). But has been called 0 time(s).` 'still refreshes the kernels when re-showing the panel fails' passed there: it guards the new order and does not reproduce the bug. - Mutations, each recompiled and run, then edited back: - the old order put back: the first new test fails the same way - panel first without its try/catch: the second new test fails with `expected [] to deeply equal [ [ [ { …(11) } ], …(1) ] ]` - npm test: 2866 passing, 239 pending, 0 failing Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): do not delete credentials another project declares Credentials are keyed by integration id alone, and SecretStorage is shared machine-wide, so once "Add Existing Integration" links one project's integration into another, both resolve the same config and federated token. Delete in either panel still removed both, and the other project silently lost its credentials: its SQL cells fell back to "(configure)", the storage event refreshed its kernels without the env vars, and the picker stopped offering the id. The confirmation only promises removal "from your project". `deleteConfiguration` now asks `findOtherProjectsDeclaring` first. While another project in the open workspace folders still declares the id, Delete only takes it off this project's roster and names the projects that keep the credentials. An unlink changes no credential, so no storage event refreshes kernels; the panel refreshes this project's running kernels itself, through the live refresher injected `@optional()` as `IntegrationManager` does, since the web extension binds none. Only open folders are scanned, so a linking project outside them still loses the credentials; the doc comment says so. The workspace walk moves out of `collectReusableIntegrations` into `forEachOtherProjectFile`, shared by both callers. The picker's rules and its existing tests are unchanged; only the walk's log prefix now names the module. The spec notes what Delete does to a linked integration. `resetConfiguration` clears the same shared credentials and is left for a follow-up. Verified: typecheck and lint exit 0, with no lint findings in the changed files; prettier clean on them; compile-tsc exit 0. IntegrationWebviewProvider|existingIntegrationPicker: 49 passing before, 53 after. The manager, writer and refresh-handler suites: 30 passing. Full unit suite: 2868 passing, 239 pending, 0 failing. Mutations, each restored and recompiled after: - guard removed, the old unconditional delete: only the unlink test fails, "Expected delete(anything()) to be called 0 time(s). But has been called 1 time(s)."; - refresh call removed: only the refresh test fails, "expected spy to be called once and with exact arguments"; - a guard that never deletes: the full-delete test fails, with the two existing delete token tests; - own files counted, snapshots counted, or the dedupe dropped: the project-name test fails with 'Current', 'Gamma' or a second 'Alpha'. A scratch inversify check resolves the provider with no refresher bound, as on web, and injects a bound one, as on node. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): hold back the type cards until their labels arrive 058fe8ffa replaced the webview's hard-coded English type labels with lookups in the string bundle the host posts as `loc_init`, falling back to the raw type id. The panel mounted `IntegrationTypeSelector` before that message arrived, and `storeLocStrings` sets no React state, so all 18 cards were first rendered as `clickhouse`, `pgsql`, `sql-server` and so on, until the first `update` re-rendered the panel. The doc comment on `integrationTypeLabel` said that fallback should never render. The panel now renders the selector only once it has handled `loc_init`, and the doc comment says so. The order inside that case matters and no test pins it: the panel is a legacy `ReactDOM.render` root on React 16, so `setLocStringsLoaded` re-renders synchronously inside the message listener and must run after `storeLocStrings`, or the cards render before the bundle is stored. A one-line comment above the two calls records this. The gate cannot strand the selector: `show()` is the panel's only creation site and always posts `loc_init` before `update`, and no serializer revives the panel. The "Add Existing Integration" button now waits for `loc_init` too; the e2e helper `awaitIntegrationsPanel` polls for `.integration-type-selector`, so the gate only lengthens its wait. Verified: `npm run typecheck` exit 0; `npm run lint` exit 0, no findings in the changed files; `prettier --check` clean on both files; `npm run esbuild-all` exit 0, integrations bundle built. No test is added. A throwaway jsdom harness outside the repo bundled this `IntegrationPanel.tsx` and replayed the host's message order. Before the change: 18 raw-id cards on first render, still raw after `loc_init`, labelled after `update`. After it: no cards on first render, then 18 labelled cards in the same tick as `loc_init`. With the two `loc_init` calls swapped, the cards rendered raw ids after `loc_init`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(e2e): drop add-existing assertions that can never fail `waitForNotification(pattern, timeout, true)` either resolves with the matching notification or throws: the helper rethrows when `required`, and selenium's `wait` resolves a function condition only on a truthy value. Mocha skips the `it` once `before` fails, so the two toast flags that `before` set from `result !== undefined` were always `true` by the time the `it` read them. `pickedLabel` had the same property: it re-read the label of the row the picker wait had selected because that label matched `INTEGRATION_NAME`. None of the three assertions could fail, and they suggested a gate that really lives in `before`. Await both required waits as plain statements, and drop the three variables with their assertions. `required: true` stays: a missed toast should stop the hook where it happened, and the success-toast wait also orders the file reads that follow it. The label comparison inside the picker wait stays as the real check, and so do the description and "Used in" row assertions: the wait matches on the label only, so they still check something. Verified: - `npm run compile-e2e` exit 0. It is the only script that type-checks test/e2e; typecheck, lint and format cover src/ only. - `npx prettier --check` on the file: clean. - grep: none of `noneAvailableToastShown`, `successToastShown` or `pickedLabel` remains in the file. - A scratch mocha run of this tree's compiled helper over the real `WebDriver.wait`, faking only the notification list: a required wait resolves with the matching notification, and otherwise rejects with `TimeoutError`, never with `undefined` (5 passing). A model of the hook with the old flag and with a plain `await` behaves the same: both pass when the toast shows; when it is missing, both fail in "before all" with the same timeout message and the `it` body never runs. No test is added: this only removes assertions that cannot fail. The e2e suite was not run, so the mutation checks (making either toast pattern or the label comparison unmatchable) were not done. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): type the cache-update doubles off the interface 058fe8ffa widened `IDeepnoteNotebookManager.updateProjectIntegrations` to `RawProjectIntegration[]`, whose `type` is any string, but the three doubles that capture its calls annotated the pre-widening `ProjectIntegration[]`, and so did their capture arrays. ts-mockito's `thenCall` takes `(...args: any[]) => any` and checks no signature, so nothing flagged them: the picker test that passes `pandas-dataframe` and `some-future-type` entries through verbatim ran through a callback whose declared type forbids them. The next contract change would slip past them the same way. Each double is now a `recordCacheUpdate` typed with `UpdateProjectIntegrationsFn`, the interface method's type, named like the existing `RefreshFn`, and its capture array holds `RawProjectIntegration[]`. The callback parameters stay unannotated: the alias comes from a method signature, whose parameters TypeScript checks bivariantly, so a hand-written annotation, even the stale one, would still compile and reopen the gap. Assertions and expected literals are unchanged. The writer's `thenReturn(true)` stub, which ts-mockito types, and its zero-argument ordering double, which reads no arguments, are left as they are. Verified: - `npm run typecheck` exit 0 before and after. - Mutation: with `integrations` widened to `readonly RawProjectIntegration[]` in src/notebooks/types.ts, the old doubles still typecheck (exit 0, no errors). The retyped ones fail with exit 2 and exactly three errors, one per file, each on the line that pushes into `cacheUpdates`: existingIntegrationPicker.unit.test.ts(376,48) TS4104, integrationManager.unit.test.ts(177,31) TS2345 and projectIntegrationsWriter.unit.test.ts(262,31) TS2345. Reverted by editing: typecheck exit 0. - Bivariance, on scratch files importing the real types: the stale annotation against the method-derived alias compiles; the same callback against a plain function type fails with TS2322. - `npm run lint` exit 0, no findings in the changed files; `prettier --check` clean on all three. - `npm run compile-tsc` exit 0. The focused suites pass unchanged, 43 before and after: existingIntegrationPicker 14, IntegrationManager.addExistingIntegration 17, persistProjectIntegrations 6, addProjectIntegration 6. - `npm test`: 2870 passing, 239 pending, 0 failing. No test is added: the change only touches types, and the compiler is the check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): groom the existing-integration picker tests Drop the test that takes the name from the stored config rather than from the first project read. Its one project declares the id once, so it cannot tell a first read from a later one, and the dedupe test already expects the stored name over two declared ones. Drop the attach test that replaces an id the file already declares. The rule lives in `addProjectIntegration`, whose own test pins it with the same arrangement; attaching only builds the entry and delegates. The dropped test also checked only the file, which cannot show a stray key on an entry: `serializeDeepnoteFile` strips unknown keys. Merge the two remaining attach tests. Both attached the same entry and checked the cache and the file the same way; only the entries the file held before differed. The merged test starts from all three existing entries, the `pandas-dataframe` and `some-future-type` ones included, and keeps the check of the returned result. `ProjectIntegration` is no longer used, so its import goes. Compare whole entries in the exclusion test, not only ids. The current project and project-a both declare `bq-oauth`, so the id is offered whether or not the current project's own file is visited; visiting it only adds `project-current` to `projectNames`, which the test now pins. Give the unsupported-types test a declaration only the type check can skip. Its one unsupported entry had no stored config, so the missing-config branch skipped it with or without the check. Project-b now declares `pg-shared` with a type this build cannot manage; without the check, that declaration counts as a type conflict and hides `pg-shared` everywhere. Turn the three cancellation tests into one table of three rows. They shared one skeleton and differed only in the files on disk, the hook that trips the token and the files read before the stop. The last-file row now also checks the reads, which its test did not. Verified: - `npm run typecheck` exit 0; `npm run lint` exit 0, no findings in the file; `npx prettier --check` clean; `npm run compile-tsc` exit 0. - existingIntegrationPicker: 11 passing, down from 14 (three tests dropped; the table keeps three cases). - Mutation harness of the test-grooming pass: one find/replace in the production code per run, compile, then the seven focused suites (148 tests, all passing unmutated). Newly caught: - the configurable-type check removed: the unsupported-types test. No test in the whole suite failed before. - the shared walk visiting the current project's own files: the exclusion test, besides the three that already failed. - `collectReusableIntegrations` alone scanning with a project id that matches nothing: the exclusion test. No test in the whole suite failed before. Still caught after the drops and merges: - the declared name winning over the stored one, or each later declaration renaming the candidate: the dedupe test. - `return` instead of `continue` on a missing stored config: the unsupported-types test. - a declared id appended again, or its old entry kept: the writer's replace test. - attach passing `projectNames` through, naming the entry after its id or using the integration id as the project id; the writer stamping one list over every file, caching the new entry alone, keying the cache by the integration id, never assigning the merged list, or pruning types it cannot manage: the merged test. The other tests that caught these before still do. - the cancellation checks: dropping the per-file check fails the mid-scan row; dropping the final check fails the last-file and discovery rows; moving it after each visited file fails the discovery row; re-checking only after an empty discovery fails the last-file row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): check what the add-existing manager tests claim Make the early-failure rows check that no file is read. Their title says each branch fails without reading or writing any file, but the body checked only writes, and the fs double does not record reads. The scan is the only step that reads files before the picker, and it runs inside `withProgress`, so each row now verifies that `withProgress` is never called. Check the cache only in the late-failure row that can see it. In the writer-throws row the cache double throws before it records anything, so `cacheUpdates` stayed empty whatever the manager did. A row field now limits that assertion to the failed-write row, and a comment on the other row says why it has none. Drop the sibling-warning test's check that the sibling file was not written. The fs double rejects writes to that file before recording them, so the check held whatever the manager did. Pin the conflict warning in the type-mismatch test. It accepted any warning, so one counting the offered integrations instead of the skipped ones ("0 integration(s) were skipped ...") passed. The test now expects `addExistingIntegrationConflictsSkipped(1)`; the `l10n.t` mock formats the count, so the pinned text is the real one. Verified: - `npm run typecheck` exit 0; `npm run lint` exit 0, no findings in the file; `npx prettier --check` clean; `npm run compile-tsc` exit 0. - IntegrationManager.addExistingIntegration: 17 passing, as before. - Mutation harness of the test-grooming pass: one find/replace in the production code per run, compile, then the seven focused suites (148 tests, all passing unmutated). Newly caught: - the snapshot guard moved after the scan, so every `.deepnote` file is read before the refusal: the snapshot row. No test in the whole suite failed before. - the conflict warning counting the offered integrations: the type-mismatch test. None of the focused suites failed before. Still caught: - each early-failure branch by its own row: the missing-notebook, closed-notebook, no-ids and uncached-project messages, and the snapshot guard applied only when no URI is passed. - going back to a roster write that updates the cache first: the failed-write row, on the cache assertion, and the test that keeps an entry the file gained while the picker was open. - no catch around the attach: the writer-throws row. - a failed write showing no error, or refreshing anyway: the failed-write row. Completion telemetry without the integration type: both late-failure rows and the link test. - the sibling warning needing two failed siblings: the sibling-warning test. - the conflict warning needing two conflicts: the type-mismatch test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): drop the bundle-key change detector Drop the test that pins the format of the label keys. The compiler already fixes it: `IntegrationTypeLabelKey` is a template literal type, `integrationTypeLabelKey` returns it, and `typeLabels` is declared `satisfies Record<IntegrationTypeLabelKey, string>`, so a missing, extra or misspelled key does not compile. The host and the webview derive the key with that same function, so the only change this test alone caught was a consistent rename of the format, which works end to end. Its comment goes with it, and so does the `integrationTypeLabelKey` import, which only it used; `noUnusedLocals` rejects it otherwise. Verified: - `npm run typecheck` exit 0, and exit 2 with TS6133 if the import is kept; `npm run lint` exit 0, no findings in the file; `npx prettier --check` clean; `npm run compile-tsc` exit 0. - integrationTypeLabels: 1 passing, down from 2. - Mutation harness of the test-grooming pass: one find/replace in the production code per run, compile, then the seven focused suites (147 tests, all passing unmutated). - the key function returning one constant key: still fails the label-coverage test and the SQL big-query label test. - the runtime predicate no longer excluding `pandas-dataframe`: still fails the label-coverage test. - a label entry removed, one added, or the prefix changed in the function alone: tsc still rejects each (TS1360, TS2353, TS2322). - the format renamed in the type, the function and all 18 keys: compiles, and no test in the whole suite fails (2866 passing, 239 pending), as intended. Before, only the dropped test did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(sql): pin the snapshot refusal message The snapshot test accepted any error message, so the guard could show the add-existing command's `addExistingIntegrationSnapshotUnsupported`, which `localize.ts` declares right next to this command's own `switchIntegrationSnapshotUnsupported`: a realistic copy-paste swap. The test now expects `switchIntegrationSnapshotUnsupported`, as the manager's snapshot row already expects its own message. Verified: - `npm run typecheck` exit 0; `npm run lint` exit 0, no findings in the file; `npx prettier --check` clean; `npm run compile-tsc` exit 0. - SqlCellStatusBarProvider: 67 passing, as before. - Mutation harness of the test-grooming pass: one find/replace in the production code per run, compile, then the seven focused suites (147 tests, all passing unmutated). - the guard showing the add-existing message instead of its own: now fails the snapshot test. No test in the whole suite failed before. - the guard disabled, or showing its message but carrying on: still fails the snapshot test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): replace this branch's force-cast fixtures with typed values Apply #544's treatment to the fixtures this branch added: - The snapshot refusal test in `sqlCellStatusBarProvider.unit.test.ts` stubbed `getProjectForNotebook` with `{ project: { integrations } } as any`, the shape #544 rebuilt 18 times in the same file. It now returns `createDeepnoteFile({ project: createDeepnoteProject(...) })` with the same integrations array. - The closed-notebook case in `integrationManager.unit.test.ts` built its visible editor as `{ notebook } as NotebookEditor`. It is now a ts-mockito `mock<NotebookEditor>()` instance whose `notebook` returns the same document, as other suites stub editors. - The `addProjectIntegration` tests read `writes.get(uri)!.project`; they use `?.` now, so a missing write fails the deep-equal instead of throwing a TypeError. Left as is: the snapshot test's `showQuickPick` stub keeps `as any`, like the 15 identical stubs #544 left in that file, and the `persistProjectIntegrations` suite's `!` reads, which predate this branch. Verified: - `npm run typecheck` exit 0; oxlint and prettier clean on the three files. - SqlCellStatusBarProvider, IntegrationManager.addExistingIntegration and the writer suites: 96 passing, as before. - The fixtures still reach the code under test: making the snapshot guard carry on after its message, or dropping the manager's closed-notebook check, fails exactly the rewritten test in each suite (quick pick shown; 'completed' instead of 'failed'). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): move each notebook's cached list with its own file's write `addProjectIntegration` merged into each file on disk, but then stamped the active file's merged list into every cached notebook of the project. Saving a notebook rebuilds its whole file from its cache entry, so a sibling holding entries the active file lacks lost them on its next save. When that sibling had unsaved edits the loss was immediate: the sweep's own flush saved it from the stamped cache before the merge re-read the file, and the command still reported success. The writer now updates the cache entry of the notebook open from each file right after that file's write, with the list that file now holds (`IDeepnoteNotebookManager.updateProjectIntegrationsForNotebook`). The active file's entry still moves before the sibling sweep starts, and a file that could not be written still leaves its entry alone. The open-document lookup moves out of `flushNotebookDocumentIfDirty` into `findOpenNotebookDocument`, so both use the same URI comparison. The cache update sits after the disk write's try block, so a failing update propagates as before instead of being reported as a failed write. `persistProjectIntegrations` keeps its project-wide `updateProjectIntegrations` for now. Verified: - New writer tests, run against a real `DeepnoteNotebookManager`, failed before the fix: a dirty sibling declaring `[int-sibling]` was written as `[int-added]`, and a clean sibling's cache held the active file's `[int-own, int-added]`. Both pass now. - The manager, picker and SQL status bar suites now observe the per-notebook update (projectId, notebookId, list) instead of the project-wide one; DeepnoteNotebookManager gains tests for the new method (single entry, uncached entry, deep clone). - Affected suites: 162 passing. `npm run compile-tsc` and `npm run typecheck` exit 0; oxlint and prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): edit only the panel's own entry in the project files Every panel Save, Reset and Delete rebuilt the project's integrations list from the panel's in-memory map and stamped it over the cache, the active file and every sibling. The map is built by the detector, which skips every type the panel cannot configure, and is only refreshed by `show()`. So any panel action deleted `pandas-dataframe` and unknown-type entries (which `RawProjectIntegration` and `addProjectIntegration` deliberately keep), and an integration linked while the panel was open was dropped by its next save. Each action now edits the entry it acts on, through the same per-file merge `addProjectIntegration` already used: - Save adds the saved integration, or replaces its entry in place. Replacing in place is new: the merge used to move a re-added id to the end of the list. - Delete takes the id off each file (`removeProjectIntegration`). - Reset writes no project file; it only clears credentials. `persistProjectIntegrations` and the project-wide `IDeepnoteNotebookManager.updateProjectIntegrations` are gone. The stamp was also what made a new integration reach a running kernel: the credential save refreshes the kernels right away, and the old code updated the cache before any await, so that refresh saw the new integration. The list now reaches the cache only after the file write, so a Save refreshes the project's kernels again once its file declares the integration. The ordering comment in the manager keeps only its real reason: a busy kernel would hold the panel back. The specs describe the per-entry edits, and the live-refresh spec now lists edits to the integrations list as a refresh trigger alongside the storage and file triggers. Verified: - Four new panel tests failed before the change. A save wrote only `[pg-1]` over `[pg-1, pg-linked, duckdb, future]`, a delete wrote `[]`, a reset rewrote the file, and nothing refreshed the kernels after a save. All four pass now. - The coverage that tested only `persistProjectIntegrations` now runs through `addProjectIntegration`: no open folder, an active file outside the folders, a failing sibling, a sibling whose flush swaps its project. The writer also gains an in-place test and a `removeProjectIntegration` suite. - Putting back the old re-add ordering (filter, then append) fails the writer's and the panel's ordering tests. - Integrations, SQL status bar and notebook manager suites: 197 passing. `npm run typecheck` and `npm run compile-tsc` exit 0; oxlint and prettier clean on the changed sources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): name the projects a shared reset, sign-out or save affects Credentials and federated refresh tokens are keyed by integration id alone, so once Add Existing Integration links an id into several projects, a Reset, Sign out or Save in one of them acts on all of them. Delete already checked `findOtherProjectsDeclaring` and only unlinks. Reset and Sign out still wiped the shared credential or token with no sign of it, and Save changed it for every project just as silently: the other projects' SQL cells started failing, or ran against new credentials, and nothing said why. The actions still go ahead. Reset is how a user removes credentials, and Save is how a shared credential gets fixed. Instead, each one now names the other projects in its success message: - Reset: "These credentials were shared, so this also removed them from: …" - Sign out: "This sign-in was shared, so this also signed out: …" (Sign out posted no message before and still posts none when nothing else shares the token.) - Save of an integration the panel already lists: "These credentials are shared, so this also changed them for: …". A brand-new integration has a fresh id, so it skips the scan. The scan call moves into `findOtherProjectsSharing`, which Delete uses too. Verified: - New tests (active project and "Alpha" both declare `bq-shared`): reset, sign-out and save each still act and name Alpha. All three failed before the change; a sign-out Alpha does not share posts no message. - Integrations and SQL status bar suites: 192 passing. `npm run typecheck` exit 0; oxlint, prettier and `npm run spell-check` clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): never fall back to another notebook when the given one is closed `resolveDeepnoteNotebook` looked up the notebook URI a menu or the panel passed, and when that notebook was no longer open it fell back to the focused editor, then to the one visible editor. Only `addExistingIntegration` guarded against that, by comparing the result with the URI afterwards. Manage Integrations, including the re-show after a link, opened the panel for whatever project the focused editor belonged to, so its next Save or Delete landed in the wrong project. The resolver now treats a given URI as the only notebook to act on: when it is not open, the command stops with "The notebook this was run for is no longer open". The focused and single-visible fallbacks remain, but only when no URI was given (the command palette). The resolver reports its own refusal, so both commands just return, and the manager's separate URI check is gone. The message constant loses its Add-Existing-only name and wording now that both commands show it. Verified: - New Manage Integrations test: the handler, given the URI of a closed notebook while another is focused, opened the panel for the focused one before the change; it now refuses with the message. A second test keeps the palette path (no URI) opening the panel for the focused notebook. - The Add Existing "no notebook open" case now passes no URI, matching the palette; with a URI it is the closed-notebook case, which still refuses. - IntegrationManager suites: 19 passing. `npm run typecheck` exit 0; oxlint and prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): refuse snapshots once, in the notebook resolver `*.snapshot.deepnote` matches the notebook selector, and the Manage Integrations toolbar button is gated only on the notebook type, so the panel opened on a snapshot. Its roster edits were all skipped (the writer never edits a snapshot), while Save and Delete still wrote and deleted credentials, and the user got "Failed to save integrations" after the credentials had already changed. Add Existing Integration had its own snapshot check; Manage Integrations had none. `resolveDeepnoteNotebook` now refuses a snapshot for both commands, after either lookup path, and Add Existing's own check is gone. The message is reworded for both commands ("Integrations cannot be managed from a snapshot file…") and renamed from its Add-Existing-only name. The SQL cell's switch keeps its own refusal: it guards a cell-metadata edit, not the integrations panel. The spec's Integration Manager section now states both resolver refusals: a closed notebook URI (previous commit) and a snapshot. Verified: - New Manage Integrations test: with a snapshot focused, the palette command opened the panel for it before the change. It now refuses with the message, and the panel never opens. - The Add Existing snapshot case still refuses, now through the resolver. - IntegrationManager and SqlCellStatusBarProvider: 87 passing. `npm run typecheck` exit 0; oxlint and prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): refresh an open panel after a link instead of opening one After a successful link, `refreshAfterProjectIntegrationsChange` called `showIntegrationsUI`, which reaches `IIntegrationWebviewProvider.show`. `show` creates and focuses a panel when none is open. So running "Deepnote: Add Existing Integration" from the command palette (it has no menu entry) ended by opening the Integrations tab over the notebook and taking focus. The re-show only guarded against a stale panel writing its list back, which cannot happen when no panel is open. It no longer happens when one is, either: panel edits now touch only their own entry. `IIntegrationWebviewProvider` gains `refresh(projectId, integrations)`, which re-renders a panel already open for that project without revealing it, and does nothing otherwise. The manager detects the project's integrations and calls it; the kernel env refresh still comes after it, so a busy kernel cannot hold the panel back. The panel's "Add Existing Integration" button, which runs the same command with the panel open, now gets its list refreshed in place. The E2E suite drives exactly this palette path. It now watches the editor tabs for 3 s after the success message (the old command opened the panel within a few hundred ms of it). It asserts the Integrations tab never appeared, that the target notebook's tab was seen (so an unreadable tab strip cannot pass the check vacuously), and that the notebook is still the active editor. The spec's Add Existing step 4 is updated. Verified: - The manager tests now expect `refresh` for this project and `show` never. Both refresh tests failed before the manager change (refresh called 0 times, since `show` ran instead) and pass now. - New provider tests: `refresh` opens no panel, re-renders an open panel of the project without revealing it, and leaves another project's panel alone. Adding a `reveal()` or dropping the project check fails them. - Integrations and SQL status bar suites: 198 passing. `npm run typecheck` and `npm run compile-e2e` exit 0; oxlint and prettier clean. - The E2E is compiled, not run: this workspace has no display or Electron libraries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * fix(integrations): send the panel's loc strings whenever its webview starts The type grid and the "Add Existing Integration" button render only after `loc_init`, which the extension posted exactly once, right after setting the panel's HTML. A message posted before the React app attaches its listener is lost. That happens on "Developer: Reload Webviews", after moving the tab to another window, and plausibly when the 1.6 MB bundle takes longer than VS Code's 200 ms inner-frame fallback. The panel was then left with no way to add or link an integration until it was closed and reopened. As `webviewHost.ts` does for the other webviews, the panel now posts `started` from the effect that attaches its message listener, so once per mount. The extension answers with the loc strings and then the list. The one-off `loc_init` at panel creation is gone, so the loc strings have one trigger. `show()` still posts the list straight away, as before. Verified: - New host test: a `started` message is answered with `loc_init` then `update`. Before the change it was ignored and nothing was posted. - Integrations suites: 132 passing. `npm run typecheck` exit 0; oxlint and prettier clean. `npm run esbuild-all` exit 0, and the rebuilt integrations bundle posts `started`. - The webview side has no component-test setup in this repo, so its mount behaviour is checked only by the E2E suites that wait for the type grid (not run here). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(e2e): treat a torn read as not yet in the declared-integration poll `awaitDeclaredIntegrationId` read and parsed the project file every 500 ms with no try/catch, while the extension rewrites that file in place (`workspace.fs.writeFile` is not atomic, and a flush save may write it first). A tick that landed mid-write threw out of the `before` hook. Mocha's `retries: 1` retries tests, not hooks, so one torn read failed the whole suite with a parse error unrelated to the feature. A read that fails now counts as "not written yet"; the poll carries on until the deadline, and the timeout message includes the last read error if there was one. Verified: - `npm run compile-e2e` exit 0; prettier clean. - The failure it guards against, reproduced on the suite's own fixture: `deserializeDeepnoteFile` throws on an empty file ("Expected object, received null") and on the first half of quick-notes.deepnote ("Invalid discriminator value"), and parses the whole file. - Not run: this workspace has no display or Electron libraries for ExTester. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv * test(integrations): share one project-file fake across the integration suites Five suites each carried their own copy of the same `workspace.fs` fake: serve `.deepnote` files by `fsPath`, reject any other read, and parse every write into a map. The writer suite's `stubWorkspace` (#429) was the original; this branch added four more copies: the picker's `stubWorkspace`, the manager suite's `setup()`, `stubProjectFileOnDisk` and `stubProjectFiles`. `stubDeepnoteFiles` in `src/test/mocks/vscodeFs.ts` now holds that core. It is built on `stubReadFile`, which now rejects a URI its resolver returns `undefined` for (its two callers always return a string). The options cover what the copies added: `failWriteFor` (writer, manager), `onRead` (picker) and `onWrite` (writer). Each suite keeps its own `findFiles`, workspace-folder and open-notebook wiring, which is where they really differ. The manager suite's failed writes now reject with "write failed for" instead of "write blocked for"; no test reads the message. The helper uses `fsPath`, which `local-rules/dont-use-fspath` bans outside `.node.ts` and `.test.ts` files; like the three `src/test/datascience` helpers, the file disables the rule. Verified: - `npm test`: 2879 passing, 239 pending, as before. In the five suites and the `stubReadFile` callers, the same 175 tests pass, by title. - `npm run typecheck`, `lint` (the same 27 existing warnings as HEAD), `format` and `spell-check` exit 0. - The suites go through the helper: dropping the write capture fails 19 tests across all five suites; ignoring `failWriteFor` fails 3 writer and 2 manager tests; reading it only at stub time fails the 2 manager tests that add a failure after setup; dropping `onWrite` fails the writer's cache-ordering test; dropping `onRead` fails the picker's 2 mid-scan cancellation tests. - Not pinned by a test: serving an unknown path as an empty file instead of rejecting passes everything, because the code under test treats an unparsable file like an unreadable one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DiYErQ9fP3wNhrNaRZAdqv --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: tomas <tomas@kislan.sk>
Migrates the extension to a single notebook per
.deepnotefile model, removing the multi-notebook-per-file machinery (the?notebook=<id>URI selection and its timers/manager state).What changes
(projectId, notebookId)lookup..deepnoteof the project on disk (open or closed), with file-watcher self-write suppression.notebook.uri.toString(), 1:1 with the kernel/controller). Environment deletion now actually stops every server using it (including closed-but-running ones), fixing a real bug.WeakSet<IKernel>, not a persistent project flag).Review hardening & polish
Since the initial build, the branch has been through review and a correctness / UX pass:
latestout of the window.{project}-{notebook}.deepnotefrom the project name instead of accumulating sibling names.file-code; legacy multi-notebook files usebookand carry a "(legacy)" tooltip (the oldnotebookcodicon rendered as a clock at tree size).DeepnoteTreeDataProvideris constructor-injected for testability, and the split prompt now flags the multi-notebook layout as legacy.How it was built
95 commits: the feature first landed in 7 sequential, independently-reviewed chunks, followed by the review-driven hardening pass above, a full ExTester E2E suite, and a comment / style cleanup.
Verification
tscclean (0 errors),eslintclean,prettier --checkclean, both esbuild bundles build.🤖 Generated with Claude Code
https://claude.ai/code/session_01AGod1izfU9yA8ZQfxD5yeZ
Summary by CodeRabbit
.deepnotefiles by project and shifts key actions to notebook-scoped workflows (create/duplicate/rename/delete/import/export), with project renaming staying group-level.